package host.skiree.cron;

import com.cronutils.descriptor.CronDescriptor;
import com.cronutils.model.CronType;
import com.cronutils.model.definition.CronDefinitionBuilder;
import com.cronutils.parser.CronParser;
import org.apache.commons.lang.StringUtils;
import org.quartz.CronExpression;

import java.text.ParseException;
import java.util.*;

public class CronExpressionUtil {

    public static void main(String[] args) {

        String cronExpression = "30 12 6 L 1 ? 2024";
        HashMap<String, Object> cec1 = cronExpressionCheck("zh-CN", cronExpression);
        HashMap<String, Object> cec2 = cronExpressionCheck("en-US", cronExpression);
        System.out.println(cec1);
        System.out.println(cec2);
    }

    private static final Map<String, String> GLOBAL_LANGUAGE = new HashMap<>();

    static {
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_success", "校验成功");
        GLOBAL_LANGUAGE.put(Locale.US.toLanguageTag() + "_success", "verification successful");

        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_exp", "非法的 Cron 表达式格式");
        GLOBAL_LANGUAGE.put(Locale.US.toLanguageTag() + "_exp", "illegal cron expression format");

        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_1", "不支持在月份的其他日期中使用 'L' 和 'LW' 进行指定");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_2", "不支持在一周的其他日期中使用 'L' 进行指定");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_3", "不支持指定多个 'nth' 天.");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_4", "表达式意外结束.");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_5", "不支持同时指定星期几和月份中的日期参数.");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_6", "非法的 Cron 表达式格式 (");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_7", "分钟和秒钟的值必须在0和59之间");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_8", "小时值必须在0和23之间");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_9", "月份中的日期值必须在1和31之间");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_10", "月份值必须在1和12之间");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_11", "星期几的值必须在1和7之间");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_12", "起始年份必须小于结束年份");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_13", "遇到意外的类型");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_14", "增量大于");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_15", "此处不允许使用 'L' 选项. (位置=");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_16", "此处不允许使用 'W' 选项. (位置=");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_17", "在大于31的值上使用 'W' 选项没有意义（一个月的最大天数）");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_18", "此处不允许使用 '#' 选项. (位置=");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_19", "在 '#' 选项后必须跟着一个介于1和5之间的数字值");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_20", "'/' 必须后跟一个整数.");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_21_1", "意外字符 '");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_21_2", "' 在 '/' 之后");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_22", "无效的月份值");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_23", "无效的星期值");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_24", "该位置的字符不合法");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_25", "在 '?' 后面有不合法的字符");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_26", "'?' 只能用于指定月份中的某天或星期中的某天");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_27", "意外的字符串结尾");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_28", "从月底的偏移量必须小于等于30");
        GLOBAL_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_29", "意外字符");
    }

    public static String getLocaleLanguageTag(String acceptLanguage) {
        return getLocaleLanguage(acceptLanguage).toLanguageTag();
    }

    public static Locale getLocaleLanguage(String acceptLanguage) {
        if (acceptLanguage == null) {
            return Locale.SIMPLIFIED_CHINESE;
        }
        acceptLanguage = acceptLanguage.toLowerCase();
        if (acceptLanguage.contains("zh-cn")) {
            return Locale.SIMPLIFIED_CHINESE;
        } else if (acceptLanguage.contains("en-us")) {
            return Locale.US;
        }
        return Locale.SIMPLIFIED_CHINESE;
    }

    public static String gBLT(String localeTag, String mark) {
        return GLOBAL_LANGUAGE.get(localeTag + "_" + mark);
    }

    public static HashMap<String, Object> cronExpressionCheck(String acceptLanguage, String expression) {
        String localeTag = getLocaleLanguageTag(acceptLanguage);
        HashMap<String, Object> res = new HashMap<>(4);
        String msg = gBLT(localeTag, "success");
        boolean mark = true;
        try {
            CronExpression.validateExpression(expression);
        } catch (Exception e) {
            mark = false;
            msg = e.getLocalizedMessage();
            if (!localeTag.equalsIgnoreCase(Locale.US.toLanguageTag())) {
                msg = cronExpressionCheckMsg(localeTag, expression, msg);
            }
        }
        res.put("cron", expression);
        res.put("flag", mark);
        res.put("msg", msg);
        res.put("exp", mark ? getExp(localeTag, expression) : gBLT(localeTag, "exp"));
        return res;
    }

    private static String getExp(String localeTag, String expression) {
        if (Locale.SIMPLIFIED_CHINESE.toLanguageTag().equalsIgnoreCase(localeTag)){
            return CronExpParser.translateCron(localeTag, expression);
        }
        return CronDescriptor.instance(Locale.US).describe(new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ)).parse(expression));
    }

    private static String cronExpressionCheckMsg(String localeTag, String expression, String msg) {
        try {
            new MyCronExpression(localeTag).buildExpression(expression);
        } catch (Exception e) {
            return e.getLocalizedMessage();
        }
        return msg;
    }

    static class CronExpParser {

        private static final Map<String, String> PARSER_LANGUAGE = new HashMap<>();

        private final static Integer HOURS = 24;

        private final static Integer TIMESCALE = 60;

        static {
            PARSER_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_0", "秒");
            PARSER_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_1", "分");
            PARSER_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_2", "点");
            PARSER_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_3", "天");
            PARSER_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_4", "月");
            PARSER_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_5", "周");
            PARSER_LANGUAGE.put(Locale.SIMPLIFIED_CHINESE.toLanguageTag() + "_6", "年");
        }

        public static String LocaleMsg(String localeTag, String mark) {
            return PARSER_LANGUAGE.get(localeTag+"_"+mark);
        }

        public static String translateCron(String localeTag, String cronExp) {
            String[] tmpCorns = cronExp.split(" ");
            StringBuffer sBuffer = new StringBuffer();
            if (tmpCorns.length == 6){
                tmpCorns = Arrays.copyOf(tmpCorns, 7);
                tmpCorns[6] = "*";
            }

            // 处理标识位年
            String year = tmpCorns[6];
            if ((!year.equals("*") && !year.equals("?"))) {
                sBuffer.append(year).append(LocaleMsg(localeTag,"6"));
            }

            // 处理标识位月
            String months = tmpCorns[4];
            if (!months.equals("*") && !months.equals("?")) {
                if (months.contains("/")) {
                    sBuffer.append("从").append(months.split("/")[0]).append("号开始").append(",每")
                            .append(months.split("/")[1]).append(LocaleMsg(localeTag,"4"));
                } else {
                    if(sBuffer.toString().contains("-")) {
                        sBuffer.append("每年");
                    }
                    sBuffer.append(months).append(LocaleMsg(localeTag,"4"));
                }
            }

            String dayofWeek = tmpCorns[5];
            if (!dayofWeek.equals("*") && !dayofWeek.equals("?")) {
                if (dayofWeek.contains(",")) {
                    sBuffer.append("每周的第").append(dayofWeek).append(LocaleMsg(localeTag,"3"));
                } else if (dayofWeek.contains("L") && dayofWeek.length() > 1) {
                    String weekNum = dayofWeek.split("L")[0];
                    String weekName = judgeWeek(localeTag,weekNum);
                    sBuffer.append("每月的最后一周的");
                    sBuffer.append(weekName);
                } else if (dayofWeek.contains("-")) {
                    String[] splitWeek = dayofWeek.split("-");
                    String weekOne = judgeWeek(localeTag,splitWeek[0]);
                    String weekTwo = judgeWeek(localeTag,splitWeek[1]);
                    sBuffer.append("每周的").append(weekOne).append("到").append(weekTwo);
                } else {
                    if ("L".equals(dayofWeek)) {
                        dayofWeek = "7";
                    }
                    sBuffer.append("每周的");
                    String weekName = judgeWeek(localeTag,dayofWeek);
                    sBuffer.append(weekName);
                }
            }

            String days = tmpCorns[3];
            if (!days.equals("?") && !days.equals("*")) {
                if (days.contains("/")) {
                    sBuffer.append("每周从第").append(days.split("/")[0]).append("天开始").append(",每")
                            .append(days.split("/")[1]).append(LocaleMsg(localeTag,"3"));
                } else if ("L".equals(days)) { // 处理L 每月的最后一天
                    sBuffer.append("每月最后一天");
                } else if (days.contains("L-")) { // 处理L-n 每月的倒数第n天
                    sBuffer.append("每月的倒数第").append(days.replace("L-", "")).append("天");
                } else if ("W".equals(days)) { // 处理W 暂时不懂怎么处理
                    sBuffer.append(days);
                } else if (days.contains("#")) {
                    String[] splitDays = days.split("#");
                    String weekNum = splitDays[0]; // 前面数字表示周几
                    String weekOfMonth = splitDays[1]; // 后面的数字表示第几周 范围1-4 一个月最多4周
                    String weekName = judgeWeek(localeTag,weekNum);
                    sBuffer.append("每月第").append(weekOfMonth).append(LocaleMsg(localeTag,"5")).append(weekName);
                } else {
//					sBuffer.append("每月").append(days).append(cronTimeArr[3]);
                    sBuffer.append("每月").append(days).append("号");
                }
            } else {
                if ((sBuffer.toString().length() == 0 || tmpCorns.length == 7) && !sBuffer.toString().contains("星期")) { // 前面没有内容的话，拼接下
                    sBuffer.append("每").append(LocaleMsg(localeTag,"3"));
                }
            }

            String hours = tmpCorns[2];
            if (!hours.equals("*")) {
                if (hours.contains("/")) {
                    // sBuffer.append("内，从").append(hours.split("/")[0]).append("时开始").append(",每")
                    // .append(hours.split("/")[1]).append(cronTimeArr[2]);
                    sBuffer.append(appendGapInfo(localeTag, hours, HOURS, 2));
                } else {
                    if (!(sBuffer.toString().length() > 0)) { // 对于 , 的情况，直接拼接
                        sBuffer.append("每天").append(hours).append(LocaleMsg(localeTag,"2"));
                    } else {
                        sBuffer.append(hours).append(LocaleMsg(localeTag,"2"));
                    }
                }
            }


            String minutes = tmpCorns[1];
            if (!minutes.equals("*")) {
                if (minutes.contains("/")) {
                    // sBuffer.append("内，从第").append(minutes.split("/")[0]).append("分开始").append(",每")
                    // .append(minutes.split("/")[1]).append(cronTimeArr[1]);
                    sBuffer.append(appendGapInfo(localeTag, minutes, TIMESCALE, 1));
                } else if (minutes.equals("0")) {
                } else if (minutes.contains("-")) {
                    String[] splitMinute = minutes.split("-");
                    sBuffer.append(splitMinute[0]).append(LocaleMsg(localeTag,"1")).append("到").append(splitMinute[1])
                            .append(LocaleMsg(localeTag,"1")).append("每分钟");
                } else {
                    sBuffer.append(minutes).append(LocaleMsg(localeTag,"1"));
                }
            }

            String seconds = tmpCorns[0];
            if (!seconds.equals("*")) {
                if (seconds.contains("/")) {
                    // sBuffer.append("内，从第").append(seconds.split("/")[0]).append("秒开始").append(",每")
                    // .append(seconds.split("/")[0]).append(cronTimeArr[0]);
                    sBuffer.append(appendGapInfo(localeTag, seconds, TIMESCALE, 0));
                } else if (!seconds.equals("0")) {
                    sBuffer.append(seconds).append(LocaleMsg(localeTag,"0"));
                }
            }

            return sBuffer.toString();
        }

        private static String appendGapInfo(String localeTag, String time, int rangeNum, int index) {
            StringBuffer sBufferTemp = new StringBuffer();
            String[] splitTime = time.split("/");
            String startNum = splitTime[0];
            String gapNum = splitTime[1];
            int endNum = rangeNum + Integer.parseInt(startNum) - Integer.parseInt(gapNum);
            String endStr = String.valueOf(endNum);
            String timeUnit = LocaleMsg(localeTag, String.valueOf(index));
            String splitTimeUnit = LocaleMsg(localeTag, String.valueOf(index));
            if (index == 1) {
                splitTimeUnit = "分钟";
            } else if (index == 2) {
                splitTimeUnit = "小时";
            }
            sBufferTemp.append("从").append(startNum).append(timeUnit).append("开始").append("到").append(endStr)
                    .append(timeUnit).append("范围内").append(",每隔").append(gapNum).append(splitTimeUnit);
            return sBufferTemp.toString();
        }

        public static String judgeWeek(String localeTag, String weekNum) {
            String weekName = "";
            if (Locale.SIMPLIFIED_CHINESE.toLanguageTag().equalsIgnoreCase(localeTag)){
                weekName = WeekEnum.matchNameCn(String.valueOf(weekNum));
            }
            if (Locale.US.toLanguageTag().equalsIgnoreCase(localeTag)){
                weekName = WeekEnum.matchNameEn(String.valueOf(weekNum));
            }
            return StringUtils.isNotEmpty(weekName) ? weekName : String.valueOf(weekNum);
        }

        public enum WeekEnum {
            SECONDS("1", "星期天", "Sunday "),
            MONDAY("2", "星期一", "Monday "),
            TUESDAY("3", "星期二", "Tuesday "),
            WEDNESDAY("4","星期三", "Wednesday "),
            THURSDAY("5", "星期四","Thursday "),
            FRIDAY("6", "星期五", "Friday "),
            SATURDAY("7", "星期六", "Saturday "),

            SECONDS_EN("SUN", "星期天", "Sunday "),
            MONDAY_EN("MON", "星期一", "Monday "),
            TUESDAY_EN("TUE", "星期二", "Tuesday "),
            WEDNESDAY_EN("WED","星期三", "Wednesday "),
            THURSDAY_EN("THU", "星期四","Thursday "),
            FRIDAY_EN("FRI", "星期五", "Friday "),
            SATURDAY_EN("SAT", "星期六", "Saturday ");

            private String key;
            private String nameCn;
            private String nameEn;

            WeekEnum(String key, String nameCn, String nameEn) {
                this.key = key;
                this.nameCn = nameCn;
                this.nameEn = nameEn;
            }

            public static String matchNameCn(String code) {
                for (host.skiree.cron.WeekEnum m : host.skiree.cron.WeekEnum.values()) {
                    if (m.getKey().equals(code)) {
                        return m.getNameCn();
                    }
                }
                return "";
            }

            public static String matchNameEn(String code) {
                for (host.skiree.cron.WeekEnum m : host.skiree.cron.WeekEnum.values()) {
                    if (m.getKey().equals(code)) {
                        return m.getNameEn();
                    }
                }
                return "";
            }

            public String getKey() {
                return key;
            }

            public void setKey(String key) {
                this.key = key;
            }

            public String getNameCn() {
                return nameCn;
            }

            public void setNameCn(String nameCn) {
                this.nameCn = nameCn;
            }

            public String getNameEn() {
                return nameEn;
            }

            public void setNameEn(String nameEn) {

                this.nameEn = nameEn;
            }
        }

    }

    static class MyCronExpression {
        String localeTag;
        static final int SECOND = 0;
        static final int MINUTE = 1;
        static final int HOUR = 2;
        static final int DAY_OF_MONTH = 3;
        static final int MONTH = 4;
        static final int DAY_OF_WEEK = 5;
        static final int YEAR = 6;
        static final int ALL_SPEC_INT = 99; // '*'
        static final int NO_SPEC_INT = 98; // '?'
        final Integer ALL_SPEC = ALL_SPEC_INT;
        final Integer NO_SPEC = NO_SPEC_INT;

        MyCronExpression(String localeTag) {
            this.localeTag = localeTag;
        }

        public String LocaleMsg(String mark) {
            return gBLT(this.localeTag, mark);
        }

        static Map<String, Integer> monthMap = new HashMap<String, Integer>(20);
        static final Map<String, Integer> dayMap = new HashMap<String, Integer>(60);

        {
            monthMap.put("JAN", 0);
            monthMap.put("FEB", 1);
            monthMap.put("MAR", 2);
            monthMap.put("APR", 3);
            monthMap.put("MAY", 4);
            monthMap.put("JUN", 5);
            monthMap.put("JUL", 6);
            monthMap.put("AUG", 7);
            monthMap.put("SEP", 8);
            monthMap.put("OCT", 9);
            monthMap.put("NOV", 10);
            monthMap.put("DEC", 11);

            dayMap.put("SUN", 1);
            dayMap.put("MON", 2);
            dayMap.put("TUE", 3);
            dayMap.put("WED", 4);
            dayMap.put("THU", 5);
            dayMap.put("FRI", 6);
            dayMap.put("SAT", 7);
        }

        transient TreeSet<Integer> seconds;
        transient TreeSet<Integer> minutes;
        transient TreeSet<Integer> hours;
        transient TreeSet<Integer> daysOfMonth;
        transient TreeSet<Integer> months;
        transient TreeSet<Integer> daysOfWeek;
        transient TreeSet<Integer> years;

        transient boolean lastdayOfWeek = false;
        transient int nthdayOfWeek = 0;
        transient boolean lastdayOfMonth = false;
        transient boolean nearestWeekday = false;
        transient int lastdayOffset = 0;
        transient boolean expressionParsed = false;

        final int MAX_YEAR = Calendar.getInstance().get(Calendar.YEAR) + 100;

        public void buildExpression(String expression) throws ParseException {
            expressionParsed = true;
            try {
                if (seconds == null) {
                    seconds = new TreeSet<Integer>();
                }
                if (minutes == null) {
                    minutes = new TreeSet<Integer>();
                }
                if (hours == null) {
                    hours = new TreeSet<Integer>();
                }
                if (daysOfMonth == null) {
                    daysOfMonth = new TreeSet<Integer>();
                }
                if (months == null) {
                    months = new TreeSet<Integer>();
                }
                if (daysOfWeek == null) {
                    daysOfWeek = new TreeSet<Integer>();
                }
                if (years == null) {
                    years = new TreeSet<Integer>();
                }

                int exprOn = SECOND;

                StringTokenizer exprsTok = new StringTokenizer(expression, " \t",
                        false);

                while (exprsTok.hasMoreTokens() && exprOn <= YEAR) {
                    String expr = exprsTok.nextToken().trim();

                    // throw an exception if L is used with other days of the month
                    if (exprOn == DAY_OF_MONTH && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) {
                        throw new ParseException(LocaleMsg("1"), -1);
                    }
                    // throw an exception if L is used with other days of the week
                    if (exprOn == DAY_OF_WEEK && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) {
                        throw new ParseException(LocaleMsg("2"), -1);
                    }
                    if (exprOn == DAY_OF_WEEK && expr.indexOf('#') != -1 && expr.indexOf('#', expr.indexOf('#') + 1) != -1) {
                        throw new ParseException(LocaleMsg("3"), -1);
                    }

                    StringTokenizer vTok = new StringTokenizer(expr, ",");
                    while (vTok.hasMoreTokens()) {
                        String v = vTok.nextToken();
                        storeExpressionVals(0, v, exprOn);
                    }

                    exprOn++;
                }

                if (exprOn <= DAY_OF_WEEK) {
                    throw new ParseException(LocaleMsg("4"), expression.length());
                }

                if (exprOn <= YEAR) {
                    storeExpressionVals(0, "*", YEAR);
                }

                TreeSet<Integer> dow = getSet(DAY_OF_WEEK);
                TreeSet<Integer> dom = getSet(DAY_OF_MONTH);

                // Copying the logic from the UnsupportedOperationException below
                boolean dayOfMSpec = !dom.contains(NO_SPEC);
                boolean dayOfWSpec = !dow.contains(NO_SPEC);

                if (!dayOfMSpec || dayOfWSpec) {
                    if (!dayOfWSpec || dayOfMSpec) {
                        throw new ParseException(LocaleMsg("5"), 0);
                    }
                }
            } catch (ParseException pe) {
                throw pe;
            } catch (Exception e) {
                throw new ParseException(LocaleMsg("6") + e.toString() + ")", 0);
            }
        }

        TreeSet<Integer> getSet(int type) {
            switch (type) {
                case SECOND:
                    return seconds;
                case MINUTE:
                    return minutes;
                case HOUR:
                    return hours;
                case DAY_OF_MONTH:
                    return daysOfMonth;
                case MONTH:
                    return months;
                case DAY_OF_WEEK:
                    return daysOfWeek;
                case YEAR:
                    return years;
                default:
                    return null;
            }
        }

        protected int skipWhiteSpace(int i, String s) {
            for (; i < s.length() && (s.charAt(i) == ' ' || s.charAt(i) == '\t'); i++) {
                ;
            }
            return i;
        }

        protected int getMonthNumber(String s) {
            Integer integer = monthMap.get(s);
            if (integer == null) {
                return -1;
            }
            return integer;
        }

        protected int getDayOfWeekNumber(String s) {
            Integer integer = dayMap.get(s);

            if (integer == null) {
                return -1;
            }

            return integer;
        }

        protected void addToSet(int val, int end, int incr, int type)
                throws ParseException {

            TreeSet<Integer> set = getSet(type);

            if (type == SECOND || type == MINUTE) {
                if ((val < 0 || val > 59 || end > 59) && (val != ALL_SPEC_INT)) {
                    throw new ParseException(LocaleMsg("7"), -1);
                }
            } else if (type == HOUR) {
                if ((val < 0 || val > 23 || end > 23) && (val != ALL_SPEC_INT)) {
                    throw new ParseException(LocaleMsg("8"), -1);
                }
            } else if (type == DAY_OF_MONTH) {
                if ((val < 1 || val > 31 || end > 31) && (val != ALL_SPEC_INT)
                        && (val != NO_SPEC_INT)) {
                    throw new ParseException(LocaleMsg("9"), -1);
                }
            } else if (type == MONTH) {
                if ((val < 1 || val > 12 || end > 12) && (val != ALL_SPEC_INT)) {
                    throw new ParseException(LocaleMsg("10"), -1);
                }
            } else if (type == DAY_OF_WEEK) {
                if ((val == 0 || val > 7 || end > 7) && (val != ALL_SPEC_INT)
                        && (val != NO_SPEC_INT)) {
                    throw new ParseException(LocaleMsg("11"), -1);
                }
            }

            if ((incr == 0 || incr == -1) && val != ALL_SPEC_INT) {
                if (val != -1) {
                    set.add(val);
                } else {
                    set.add(NO_SPEC);
                }

                return;
            }

            int startAt = val;
            int stopAt = end;

            if (val == ALL_SPEC_INT && incr <= 0) {
                incr = 1;
                set.add(ALL_SPEC); // put in a marker, but also fill values
            }

            if (type == SECOND || type == MINUTE) {
                if (stopAt == -1) {
                    stopAt = 59;
                }
                if (startAt == -1 || startAt == ALL_SPEC_INT) {
                    startAt = 0;
                }
            } else if (type == HOUR) {
                if (stopAt == -1) {
                    stopAt = 23;
                }
                if (startAt == -1 || startAt == ALL_SPEC_INT) {
                    startAt = 0;
                }
            } else if (type == DAY_OF_MONTH) {
                if (stopAt == -1) {
                    stopAt = 31;
                }
                if (startAt == -1 || startAt == ALL_SPEC_INT) {
                    startAt = 1;
                }
            } else if (type == MONTH) {
                if (stopAt == -1) {
                    stopAt = 12;
                }
                if (startAt == -1 || startAt == ALL_SPEC_INT) {
                    startAt = 1;
                }
            } else if (type == DAY_OF_WEEK) {
                if (stopAt == -1) {
                    stopAt = 7;
                }
                if (startAt == -1 || startAt == ALL_SPEC_INT) {
                    startAt = 1;
                }
            } else if (type == YEAR) {
                if (stopAt == -1) {
                    stopAt = MAX_YEAR;
                }
                if (startAt == -1 || startAt == ALL_SPEC_INT) {
                    startAt = 1970;
                }
            }

            // if the end of the range is before the start, then we need to overflow into
            // the next day, month etc. This is done by adding the maximum amount for that
            // type, and using modulus max to determine the value being added.
            int max = -1;
            if (stopAt < startAt) {
                switch (type) {
                    case SECOND:
                        max = 60;
                        break;
                    case MINUTE:
                        max = 60;
                        break;
                    case HOUR:
                        max = 24;
                        break;
                    case MONTH:
                        max = 12;
                        break;
                    case DAY_OF_WEEK:
                        max = 7;
                        break;
                    case DAY_OF_MONTH:
                        max = 31;
                        break;
                    case YEAR:
                        throw new IllegalArgumentException(LocaleMsg("12"));
                    default:
                        throw new IllegalArgumentException(LocaleMsg("13"));
                }
                stopAt += max;
            }

            for (int i = startAt; i <= stopAt; i += incr) {
                if (max == -1) {
                    // ie: there's no max to overflow over
                    set.add(i);
                } else {
                    // take the modulus to get the real value
                    int i2 = i % max;

                    // 1-indexed ranges should not include 0, and should include their max
                    if (i2 == 0 && (type == MONTH || type == DAY_OF_WEEK || type == DAY_OF_MONTH)) {
                        i2 = max;
                    }

                    set.add(i2);
                }
            }
        }

        protected int getNumericValue(String s, int i) {
            int endOfVal = findNextWhiteSpace(i, s);
            String val = s.substring(i, endOfVal);
            return Integer.parseInt(val);
        }

        protected int findNextWhiteSpace(int i, String s) {
            for (; i < s.length() && (s.charAt(i) != ' ' || s.charAt(i) != '\t'); i++) {
                ;
            }

            return i;
        }

        private void checkIncrementRange(int incr, int type, int idxPos) throws ParseException {
            if (incr > 59 && (type == SECOND || type == MINUTE)) {
                throw new ParseException(LocaleMsg("14") + "60 : " + incr, idxPos);
            } else if (incr > 23 && (type == HOUR)) {
                throw new ParseException(LocaleMsg("14") + "24 : " + incr, idxPos);
            } else if (incr > 31 && (type == DAY_OF_MONTH)) {
                throw new ParseException(LocaleMsg("14") + "31 : " + incr, idxPos);
            } else if (incr > 7 && (type == DAY_OF_WEEK)) {
                throw new ParseException(LocaleMsg("14") + "7 : " + incr, idxPos);
            } else if (incr > 12 && (type == MONTH)) {
                throw new ParseException(LocaleMsg("14") + "12 : " + incr, idxPos);
            }
        }

        protected ValueSet getValue(int v, String s, int i) {
            char c = s.charAt(i);
            StringBuilder s1 = new StringBuilder(String.valueOf(v));
            while (c >= '0' && c <= '9') {
                s1.append(c);
                i++;
                if (i >= s.length()) {
                    break;
                }
                c = s.charAt(i);
            }
            ValueSet val = new ValueSet();

            val.pos = (i < s.length()) ? i : i + 1;
            val.value = Integer.parseInt(s1.toString());
            return val;
        }

        protected int checkNext(int pos, String s, int val, int type)
                throws ParseException {

            int end = -1;
            int i = pos;

            if (i >= s.length()) {
                addToSet(val, end, -1, type);
                return i;
            }

            char c = s.charAt(pos);

            if (c == 'L') {
                if (type == DAY_OF_WEEK) {
                    if (val < 1 || val > 7)
                        throw new ParseException(LocaleMsg("11"), -1);
                    lastdayOfWeek = true;
                } else {
                    throw new ParseException(LocaleMsg("15") + i + ")", i);
                }
                TreeSet<Integer> set = getSet(type);
                set.add(val);
                i++;
                return i;
            }

            if (c == 'W') {
                if (type == DAY_OF_MONTH) {
                    nearestWeekday = true;
                } else {
                    throw new ParseException(LocaleMsg("16") + i + ")", i);
                }
                if (val > 31)
                    throw new ParseException(LocaleMsg("17"), i);
                TreeSet<Integer> set = getSet(type);
                set.add(val);
                i++;
                return i;
            }

            if (c == '#') {
                if (type != DAY_OF_WEEK) {
                    throw new ParseException(LocaleMsg("18") + i + ")", i);
                }
                i++;
                try {
                    nthdayOfWeek = Integer.parseInt(s.substring(i));
                    if (nthdayOfWeek < 1 || nthdayOfWeek > 5) {
                        throw new Exception();
                    }
                } catch (Exception e) {
                    throw new ParseException(LocaleMsg("19"), i);
                }

                TreeSet<Integer> set = getSet(type);
                set.add(val);
                i++;
                return i;
            }

            if (c == '-') {
                i++;
                c = s.charAt(i);
                int v = Integer.parseInt(String.valueOf(c));
                end = v;
                i++;
                if (i >= s.length()) {
                    addToSet(val, end, 1, type);
                    return i;
                }
                c = s.charAt(i);
                if (c >= '0' && c <= '9') {
                    ValueSet vs = getValue(v, s, i);
                    end = vs.value;
                    i = vs.pos;
                }
                if (i < s.length() && ((c = s.charAt(i)) == '/')) {
                    i++;
                    c = s.charAt(i);
                    int v2 = Integer.parseInt(String.valueOf(c));
                    i++;
                    if (i >= s.length()) {
                        addToSet(val, end, v2, type);
                        return i;
                    }
                    c = s.charAt(i);
                    if (c >= '0' && c <= '9') {
                        ValueSet vs = getValue(v2, s, i);
                        int v3 = vs.value;
                        addToSet(val, end, v3, type);
                        i = vs.pos;
                        return i;
                    } else {
                        addToSet(val, end, v2, type);
                        return i;
                    }
                } else {
                    addToSet(val, end, 1, type);
                    return i;
                }
            }

            if (c == '/') {
                if ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s.charAt(i + 1) == '\t') {
                    throw new ParseException(LocaleMsg("20"), i);
                }

                i++;
                c = s.charAt(i);
                int v2 = Integer.parseInt(String.valueOf(c));
                i++;
                if (i >= s.length()) {
                    checkIncrementRange(v2, type, i);
                    addToSet(val, end, v2, type);
                    return i;
                }
                c = s.charAt(i);
                if (c >= '0' && c <= '9') {
                    ValueSet vs = getValue(v2, s, i);
                    int v3 = vs.value;
                    checkIncrementRange(v3, type, i);
                    addToSet(val, end, v3, type);
                    i = vs.pos;
                    return i;
                } else {
                    throw new ParseException(LocaleMsg("21_1") + c + LocaleMsg("21_2"), i);
                }
            }

            addToSet(val, end, 0, type);
            i++;
            return i;
        }

        protected int storeExpressionVals(int pos, String s, int type)
                throws ParseException {

            int incr = 0;
            int i = skipWhiteSpace(pos, s);
            if (i >= s.length()) {
                return i;
            }
            char c = s.charAt(i);
            if ((c >= 'A') && (c <= 'Z') && (!s.equals("L")) && (!s.equals("LW")) && (!s.matches("^L-[0-9]*[W]?"))) {
                String sub = s.substring(i, i + 3);
                int sval = -1;
                int eval = -1;
                if (type == MONTH) {
                    sval = getMonthNumber(sub) + 1;
                    if (sval <= 0) {
                        throw new ParseException(LocaleMsg("22") + ": '" + sub + "'", i);
                    }
                    if (s.length() > i + 3) {
                        c = s.charAt(i + 3);
                        if (c == '-') {
                            i += 4;
                            sub = s.substring(i, i + 3);
                            eval = getMonthNumber(sub) + 1;
                            if (eval <= 0) {
                                throw new ParseException(LocaleMsg("22") + ": '" + sub + "'", i);
                            }
                        }
                    }
                } else if (type == DAY_OF_WEEK) {
                    sval = getDayOfWeekNumber(sub);
                    if (sval < 0) {
                        throw new ParseException(LocaleMsg("23") + ": '"
                                + sub + "'", i);
                    }
                    if (s.length() > i + 3) {
                        c = s.charAt(i + 3);
                        if (c == '-') {
                            i += 4;
                            sub = s.substring(i, i + 3);
                            eval = getDayOfWeekNumber(sub);
                            if (eval < 0) {
                                throw new ParseException(
                                        LocaleMsg("23") + ": '" + sub
                                                + "'", i);
                            }
                        } else if (c == '#') {
                            try {
                                i += 4;
                                nthdayOfWeek = Integer.parseInt(s.substring(i));
                                if (nthdayOfWeek < 1 || nthdayOfWeek > 5) {
                                    throw new Exception();
                                }
                            } catch (Exception e) {
                                throw new ParseException(
                                        LocaleMsg("19"),
                                        i);
                            }
                        } else if (c == 'L') {
                            lastdayOfWeek = true;
                            i++;
                        }
                    }

                } else {
                    throw new ParseException(
                            LocaleMsg("24") + ": '" + sub + "'",
                            i);
                }
                if (eval != -1) {
                    incr = 1;
                }
                addToSet(sval, eval, incr, type);
                return (i + 3);
            }

            if (c == '?') {
                i++;
                if ((i + 1) < s.length()
                        && (s.charAt(i) != ' ' && s.charAt(i + 1) != '\t')) {
                    throw new ParseException(LocaleMsg("25") + ": "
                            + s.charAt(i), i);
                }
                if (type != DAY_OF_WEEK && type != DAY_OF_MONTH) {
                    throw new ParseException(LocaleMsg("26"),
                            i);
                }
                if (type == DAY_OF_WEEK && !lastdayOfMonth) {
                    int val = daysOfMonth.last();
                    if (val == NO_SPEC_INT) {
                        throw new ParseException(
                                LocaleMsg("26"),
                                i);
                    }
                }

                addToSet(NO_SPEC_INT, -1, 0, type);
                return i;
            }

            if (c == '*' || c == '/') {
                if (c == '*' && (i + 1) >= s.length()) {
                    addToSet(ALL_SPEC_INT, -1, incr, type);
                    return i + 1;
                } else if (c == '/'
                        && ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s
                        .charAt(i + 1) == '\t')) {
                    throw new ParseException(LocaleMsg("20"), i);
                } else if (c == '*') {
                    i++;
                }
                c = s.charAt(i);
                if (c == '/') { // is an increment specified?
                    i++;
                    if (i >= s.length()) {
                        throw new ParseException(LocaleMsg("27"), i);
                    }

                    incr = getNumericValue(s, i);

                    i++;
                    if (incr > 10) {
                        i++;
                    }
                    checkIncrementRange(incr, type, i);
                } else {
                    incr = 1;
                }

                addToSet(ALL_SPEC_INT, -1, incr, type);
                return i;
            } else if (c == 'L') {
                i++;
                if (type == DAY_OF_MONTH) {
                    lastdayOfMonth = true;
                }
                if (type == DAY_OF_WEEK) {
                    addToSet(7, 7, 0, type);
                }
                if (type == DAY_OF_MONTH && s.length() > i) {
                    c = s.charAt(i);
                    if (c == '-') {
                        ValueSet vs = getValue(0, s, i + 1);
                        lastdayOffset = vs.value;
                        if (lastdayOffset > 30)
                            throw new ParseException(LocaleMsg("28"), i + 1);
                        i = vs.pos;
                    }
                    if (s.length() > i) {
                        c = s.charAt(i);
                        if (c == 'W') {
                            nearestWeekday = true;
                            i++;
                        }
                    }
                }
                return i;
            } else if (c >= '0' && c <= '9') {
                int val = Integer.parseInt(String.valueOf(c));
                i++;
                if (i >= s.length()) {
                    addToSet(val, -1, -1, type);
                } else {
                    c = s.charAt(i);
                    if (c >= '0' && c <= '9') {
                        ValueSet vs = getValue(val, s, i);
                        val = vs.value;
                        i = vs.pos;
                    }
                    i = checkNext(i, s, val, type);
                    return i;
                }
            } else {
                throw new ParseException(LocaleMsg("29") + ": " + c, i);
            }

            return i;
        }


        class ValueSet {
            public int value;
            public int pos;
        }

    }

}